package timer

import (
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"
)

type CronExpr struct {
	sec   uint64 //0 ~ 59
	min   uint64 //0 ~ 59
	hour  uint64 //0 ~ 23
	dom   uint64 //day of month 1 ~ 31
	month uint64 // 1 ~ 12
	dow   uint64 //day of week 0 ~ 6
}

// sec min hour dom mon dow
func NewCronExpr(expr string) (cronexpr *CronExpr, err error) {
	fields := strings.Fields(expr)            //分割表达式
	if len(fields) != 5 && len(fields) != 6 { //最多省略sec
		err = fmt.Errorf("invalid expr %v, expected 5 or 6 fields, got %v", expr, len(fields))
		return
	}

	if len(fields) == 5 { //没写sec默认添加为0
		fields = append([]string{"0"}, fields...)
	}

	cronexpr = new(CronExpr)

	//sec
	cronexpr.sec, err = parseCronField(fields[0], 0, 59)
	if err != nil {
		goto onError
	}
	//min
	cronexpr.min, err = parseCronField(fields[1], 0, 59)
	if err != nil {
		goto onError
	}
	//hour
	cronexpr.hour, err = parseCronField(fields[2], 0, 23)
	if err != nil {
		goto onError
	}
	//day of month
	cronexpr.dom, err = parseCronField(fields[3], 1, 31)
	if err != nil {
		goto onError
	}
	//month
	cronexpr.month, err = parseCronField(fields[4], 1, 12)
	if err != nil {
		goto onError
	}
	//day of week
	cronexpr.dow, err = parseCronField(fields[5], 0, 6)
	if err != nil {
		goto onError
	}
	return

onError:
	err = fmt.Errorf("invalid expr: %v, %v", expr, err)
	return
}

// 1. *   		所有值
// 2. x			单值
// 3. x-y 		范围
// 4. */d		步长,每d个单位一次
// 5. x/d 		从x开始,每d个单位一次
// 6. x-y/d		在x~y中,从x开始,每d个单位一次
func parseCronField(field string, min int, max int) (cronField uint64, err error) {
	fields := strings.Split(field, ",")

	for _, field := range fields {
		eachStep := strings.Split(field, "/")
		if len(eachStep) > 2 {
			err = fmt.Errorf("too many /: %v", field)
			return
		}

		//只有左部可能有范围,只分割左边
		startAndEnd := strings.Split(eachStep[0], "-")
		if len(startAndEnd) > 2 {
			err = fmt.Errorf("too many - : %v", eachStep[0])
			return
		}

		var start, end int
		if startAndEnd[0] == "*" {
			if len(startAndEnd) != 1 {
				err = fmt.Errorf("invalid range: %v", eachStep[0])
				return
			}
			start = min
			end = max
		} else {
			start, err = strconv.Atoi(startAndEnd[0]) //获取start
			if err != nil {
				err = fmt.Errorf("invalid range: %v", eachStep[0])
				return
			}

			if len(startAndEnd) == 1 { //单个数不是范围
				if len(eachStep) == 2 { //有步长的情况
					end = max
				} else { //单范围的情况
					end = start
				}
			} else { //求范围的第二个数
				end, err = strconv.Atoi(startAndEnd[1]) //获取end
				if err != nil {
					err = fmt.Errorf("invalid range: %v", eachStep[0])
					return
				}
			}
		}

		//范围合法性检测
		if start > end {
			err = fmt.Errorf("invalid range: %v", eachStep[0])
			return
		}
		if start < min {
			err = fmt.Errorf("%v out of range [%v:%v]", eachStep[0], min, max)
			return
		}
		if end > max {
			err = fmt.Errorf("%v out of range [%v:%v]", eachStep[0], min, max)
			return
		}

		//获取步长
		var step int
		if len(eachStep) == 1 {
			step = 1
		} else {
			step, err = strconv.Atoi(eachStep[1])
			if err != nil {
				err = fmt.Errorf("invalid step: %v", eachStep[1])
				return
			}
			if step <= 0 {
				err = fmt.Errorf("invalid step: %v", eachStep[1])
				return
			}
		}

		//填充位图
		if step == 1 {
			cronField |= ^(math.MaxUint64 << uint(end+1)) & (math.MaxUint64 << uint(start))
		} else {
			for i := start; i <= end; i += step {
				cronField |= 1 << uint(i)
			}
		}
	}

	return
}

func (e *CronExpr) matchDay(t time.Time) bool {
	if e.dom == 0xfffffffe {
		return 1<<uint(t.Weekday())&e.dow != 0
	}

	if e.dow == 0x7f {
		return 1<<uint(t.Day())&e.dom != 0
	}

	return 1<<uint(t.Day())&e.dom != 0 || 1<<uint(t.Weekday())&e.dow != 0
}

func (e *CronExpr) Next(t time.Time) time.Time {
	t = t.Truncate(time.Second).Add(time.Second)
	year := t.Year()
	initFlag := false

reloop:
	if t.Year() > year+1 { //接下来一年内没有新日期则不满足条件,返回空值
		return time.Time{}
	}

	//month
	for 1<<uint(t.Month())&e.month == 0 {
		if !initFlag {
			initFlag = true
			t = time.Date(t.Year(), t.Month(), 0, 0, 0, 0, 0, t.Location())
		}

		t = t.AddDate(0, 1, 0) //月份+1
		if t.Month() == time.January {
			goto reloop
		}
	}

	//day
	for !e.matchDay(t) {
		if !initFlag {
			initFlag = true
			t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, t.Location())
		}

		t = t.AddDate(0, 0, 1)
		if t.Day() == 1 {
			goto reloop
		}
	}

	//hour
	for 1<<uint(t.Hour())&e.hour == 0 {
		if !initFlag {
			initFlag = true
			t = t.Truncate(time.Hour)
		}

		t = t.Add(time.Hour)
		if t.Hour() == 0 {
			goto reloop
		}
	}

	//min
	for 1<<uint(t.Minute())&e.min == 0 {
		if !initFlag {
			initFlag = true
			t = t.Truncate(time.Minute)
		}

		t = t.Add(time.Minute)
		if t.Minute() == 0 {
			goto reloop
		}
	}
	//sec
	for 1<<uint(t.Second())&e.sec == 0 {
		if !initFlag {
			initFlag = true
		}

		t = t.Add(time.Second)
		if t.Second() == 0 {
			goto reloop
		}
	}

	return t
}
